/*
 * MIT License
 *
 * Copyright (c) 2023-present, tangli
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package tony.web.filter

/**
 * 重复读取请求包装器 相关
 * @author tangli
 * @date 2023/09/28 19:03
 */
import jakarta.servlet.FilterChain
import jakarta.servlet.ReadListener
import jakarta.servlet.ServletInputStream
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletRequestWrapper
import jakarta.servlet.http.HttpServletResponse
import jakarta.servlet.http.Part
import java.io.BufferedReader
import java.io.ByteArrayInputStream
import java.io.IOException
import java.io.InputStreamReader
import java.nio.charset.StandardCharsets
import java.util.Collections
import org.springframework.core.PriorityOrdered
import org.springframework.http.HttpMethod
import org.springframework.http.MediaType
import org.springframework.web.cors.CorsUtils
import org.springframework.web.filter.OncePerRequestFilter
import tony.core.utils.antPathMatchAny
import tony.core.utils.sanitizedPath
import tony.web.WebContext
import tony.web.filter.RepeatReadRequestWrapper.`-Companion`.toRepeatRead
import tony.web.utils.isTextMediaTypes
import tony.web.utils.parseMediaType

/**
 * 可重复读请求过滤器.
 * @author tangli
 * @date 2023/09/13 19:47
 */
internal class RequestReplaceToRepeatReadFilter(
    /**
     * 请求日志排除url
     */
    traceLogExcludePatterns: Set<String>,
    private val requestBodyMaxSize: Long,
) : OncePerRequestFilter(),
    PriorityOrdered {
    private val excludedUrls by lazy(LazyThreadSafetyMode.PUBLICATION) {
        traceLogExcludePatterns
            .map { sanitizedPath("${WebContext.contextPath}/$it") }
            .plus(WebContext.excludePathPatterns(WebContext.contextPath))
    }

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain,
    ) =
        filterChain.doFilter(
            request.toRepeatRead(requestBodyMaxSize),
            response
        )

    override fun shouldNotFilter(request: HttpServletRequest) =
        request
            .requestURI
            .antPathMatchAny(excludedUrls) ||
            CorsUtils.isPreFlightRequest(request)

    override fun getOrder() =
        PriorityOrdered.HIGHEST_PRECEDENCE + 1
}

/**
 * 可重复读请求包装器.
 * @author tangli
 * @date 2023/09/28 19:03
 */
public class RepeatReadRequestWrapper
    @Throws(IOException::class)
    internal constructor(
        request: HttpServletRequest,
        private val requestBodyMaxSize: Long,
    ) : HttpServletRequestWrapper(request) {
        /**
         * 是否应该缓存请求体内容
         * 只有文本类型的内容才缓存，二进制内容不缓存以节省内存
         */
        private val shouldCacheContent = shouldCacheRequestBody()

        /**
         * Don't know why must do this can get parts in controller.
         * 先把parts在初始化时就保存下来，否则会获取不到。暂时的成本低的做法。
         */
        private val initializedParts: MutableCollection<Part> =
            if (request
                    .contentType
                    ?.contains(MediaType.MULTIPART_FORM_DATA_VALUE) == true
            ) {
                request.parts
            } else {
                Collections.emptyList()
            }

        override fun getParts(): MutableCollection<Part> =
            initializedParts

        /**
         * 缓存的请求体内容，只有文本类型才缓存
         */
        private val cachedContent =
            if (shouldCacheContent && !isFormPost()) {
                request.inputStream.readBytes()
            } else {
                ByteArray(0)
            }

        public val contentAsByteArray: ByteArray
            get() {
                return if (shouldCacheContent) {
                    cachedContent.copyOf()
                } else {
                    // 对于不缓存的内容，返回空数组并记录日志
                    ByteArray(0)
                }
            }

        override fun getInputStream(): ServletInputStream =
            if (shouldCacheContent) {
                // 缓存模式：返回可重复读的流
                object : ServletInputStream() {
                    private val byteArrayInputStream =
                        ByteArrayInputStream(cachedContent)

                    override fun isReady() =
                        true

                    override fun setReadListener(listener: ReadListener?) =
                        Unit

                    override fun isFinished() =
                        byteArrayInputStream.available() == 0

                    override fun read() =
                        byteArrayInputStream.read()

                    override fun reset() =
                        byteArrayInputStream.reset()

                    override fun markSupported() =
                        byteArrayInputStream.markSupported()
                }
            } else {
                // 流式模式：直接返回原始流（不可重复读）
                super.getInputStream()
            }

        override fun getReader(): BufferedReader =
            if (shouldCacheContent) {
                BufferedReader(InputStreamReader(ByteArrayInputStream(cachedContent), StandardCharsets.UTF_8))
            } else {
                super.getReader()
            }

        override fun getRemoteAddr(): String =
            sequenceOf(
                getHeader("X-Real-IP"),
                getHeader("X-Forwarded-For")?.split(",")?.firstOrNull(),
                getHeader("ip"),
                super.remoteAddr?.split(",")?.firstOrNull()
            ).map { it?.trim() }
                .firstOrNull { !it.isNullOrBlank() && !it.equals("unknown", ignoreCase = true) }
                ?: super.remoteAddr.orEmpty()

        /**
         * 判断是否应该缓存请求体内容
         * 只有文本类型并且请求长度小于配置长度[requestBodyMaxSize]才缓存
         */
        private fun shouldCacheRequestBody(): Boolean =
            isTextMediaTypes(parseMediaType(this.contentType)) &&
                request.contentLengthLong < requestBodyMaxSize

        private fun isFormPost() =
            contentType in formPostContentTypes &&
                HttpMethod
                    .POST
                    .matches(method)

        @Suppress("ClassName")
        public companion object `-Companion` {
            @JvmStatic
            public fun HttpServletRequest.toRepeatRead(requestBodyMaxSize: Long): RepeatReadRequestWrapper =
                this as? RepeatReadRequestWrapper ?: RepeatReadRequestWrapper(this, requestBodyMaxSize)

            private val formPostContentTypes =
                arrayOf(
                    "application/x-www-form-urlencoded",
                    "multipart/form-data"
                )
        }
    }
